﻿using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using Aeon.Framework.Objects.Models.Model3DS;

namespace Aeon.Framework.Objects.Models
{

/// <summary>
/// Parser and loader for .3ds file models.
/// Caches last loaded model only.
/// 
/// [TODO] Slow. Incomplete.
/// [TODO] Remove Console.Writelines.
/// 
/// </summary>
public sealed class Parser3DS : IParserModel
    {
    // --- Inner Classes:

    /// <summary>
    /// Object for the 3ds segment structure.
    /// 
    /// </summary>
    private struct Segment
		{
        public Group ID;        // Section identifier, 2 bytes.
		public uint Length;     // Section length, 4 bytes.
		public int BytesRead;   // Bytes marker.

		public Segment(BinaryReader reader)
		    {
			ID = (Group)reader.ReadUInt16();
			Length = reader.ReadUInt32();
			BytesRead = 6; 
			}
		}

    /// <summary>
    /// Enumeration for file segments.
    /// 
    /// </summary>
    private enum Group : ushort
		{
		C_PRIMARY           = 0x4D4D,
		C_OBJECTINFO        = 0x3D3D, 
		C_VERSION           = 0x0002,
		C_EDITKEYFRAME      = 0xB000,          
		C_MATERIAL          = 0xAFFF,    
		C_MATNAME           = 0xA000, 
		C_MATAMBIENT        = 0xA010,
		C_MATDIFFUSE        = 0xA020,
		C_MATSPECULAR       = 0xA030,
		C_MATSHININESS      = 0xA040,
		C_MATMAP            = 0xA200,
		C_MATMAPFILE        = 0xA300,
		C_OBJECT            = 0x4000,   
		C_OBJECT_MESH       = 0x4100,
		C_OBJECT_VERTICES   = 0x4110, 
		C_OBJECT_FACES      = 0x4120,
		C_OBJECT_MATERIAL   = 0x4130,
		C_OBJECT_UV		    = 0x4140
		}

    // --- Attributes:

    private String previous;                // Currently cached model path.
    private Adapter3DS cache;               // Currently cached model.

    private int version;                    //
    private double maxX, maxY, maxZ;        // Model boundaries, maximum values.
    private double minX, minY, minZ;        // Model boundaries, minimum values.

    private List<Data3DS> parts;                        //
    private Dictionary<String,AeonMaterial> materials;  //
    
    // --- Constructor:

    public Parser3DS()
        {

        previous = String.Empty;
        }
    
    // --- Public Methods:
    
    /// <summary>
    /// Receives a file path and returns the 3DS model in it, or throws an exception.
    /// 
    /// </summary>
    public IModelAdapter Parse(String path)
        {        
        String error = String.Empty;
        Adapter3DS model = new Adapter3DS();

        if (path == null && path == String.Empty) { error = "Parser3DS: Empty path."; }
        else {
             if (path == previous) return(cache);

             if (!File.Exists(path)) { error = "Parser3DS: File not found at "+path+"."; }
             else {
                  String root = new FileInfo(path).DirectoryName + Path.DirectorySeparatorChar;

                  version = -1;
                  maxX = maxY = maxZ = double.MinValue;
			      minX = minY = minZ = double.MaxValue;

                  parts = new List<Data3DS>();
                  materials = new Dictionary<String,AeonMaterial>();

                  using(FileStream file = new FileStream(path, FileMode.Open, FileAccess.Read))
                       {
                       using(BinaryReader reader = new BinaryReader(file))
                            {
                            reader.BaseStream.Seek(0, SeekOrigin.Begin);
                            Segment seg = new Segment(reader);

                            if (seg.ID == Group.C_PRIMARY) { parseSegment(reader,seg); }
                            else error = "Parser3DS: "+path+" is not a valid 3ds file.";
                            }
                       }

                  Data3DS[] lst = new Data3DS[parts.Count];
                  foreach(Data3DS c in parts) { lst[lst.Length] = c; }
                  parts.Clear();

                  model.Components = lst;
                  model.Version = version;

                  model.MaxX = maxX;
                  model.MaxY = maxY;
                  model.MaxZ = maxZ;
                  model.MinX = minX;
                  model.MinY = minY;
                  model.MinZ = minZ;
                  }
             }

        if (error != String.Empty) { throw new ArgumentException(error); }
        else {
             previous = path;
             cache = model;             
             return(model);
             }
        }

    // --- Internal Methods:

    private void parseSkip(BinaryReader reader, Segment sector)
	    {
		int length = (int)sector.Length - sector.BytesRead;

		reader.ReadBytes(length);
		sector.BytesRead += length;			
	    }

	private	void parseSegment(BinaryReader reader, Segment sector)
        {
		while(sector.BytesRead < sector.Length)
		    {
			Segment child = new Segment(reader);

			switch (child.ID)
				{
                case(Group.C_OBJECT): parseString(reader,child);
						              Data3DS cmp = parseObject(reader,child);
						              cmp.CalculateNormals();						             
                                      parts.Add(cmp);
						              break;

                case(Group.C_VERSION): version = reader.ReadInt32();
				                       child.BytesRead += 4;
				                       break;

                case(Group.C_MATERIAL): parseMaterial(reader,child); break;
                case(Group.C_OBJECTINFO): /* Ignore */ break;
				default: parseSkip(reader,child); break;
				}

			sector.BytesRead += child.BytesRead;
			}
		}

    private String parseString(BinaryReader reader, Segment sector)
	    {
		StringBuilder sb = new StringBuilder ();
		Byte b = reader.ReadByte();

        for(int index=0; b!=0; index++)
            {
            sb.Append((char)b);
            b = reader.ReadByte();

            if (b!=0) { sector.BytesRead += index+1; }
            }

		return(sb.ToString());
	    }

	private UInt16 parseNumber(BinaryReader reader, Segment sector)
		{
		Segment child = new Segment(reader);
		UInt16 num = reader.ReadUInt16();

		child.BytesRead += 2;
		sector.BytesRead += child.BytesRead;

		return(num);
		}

    private IColor parseColor(BinaryReader reader, Segment sector)
		{
		Segment child = new Segment(reader);
        IColor clr = new AeonColor(0,0,0,1);

        for(short cnt=3; cnt>0; cnt--)
            {
            switch(cnt)
                {
                case(3): clr.R = (float)reader.ReadByte()/256; break;
                case(2): clr.G = (float)reader.ReadByte()/256; break;
                case(1): clr.B = (float)reader.ReadByte()/256; break;
                }
            }

        sector.BytesRead += (int)child.Length;
        return(clr);
		}

	private void parseMaterial(BinaryReader reader, Segment sector)
		{
        AeonMaterial mat = AeonMaterials.Default;
        String name = String.Empty;
			
		while(sector.BytesRead < sector.Length)
		    {
			Segment child = new Segment(reader);
				
			switch(child.ID)
				{
                case(Group.C_MATMAP): parseNumber(reader,child);
						              parseTexMap(reader,child,mat);
                                      break;
	
				case(Group.C_MATSHININESS): mat.Shininess = parseNumber(reader,child); break;
                case(Group.C_MATSPECULAR): mat.Specular = parseColor(reader,child); break;
                case(Group.C_MATDIFFUSE): mat.Diffuse = parseColor(reader,child); break;
                case(Group.C_MATAMBIENT): mat.Ambient = parseColor(reader,child); break;
				case(Group.C_MATNAME): name = parseString(reader,child); break;		
						
				default: parseSkip(reader,child); break;
				}

			sector.BytesRead += child.BytesRead;
			}

		materials.Add(name,mat);
		}

	private void parseTexMap(BinaryReader reader, Segment sector, AeonMaterial mat)
		{
		while(sector.BytesRead < sector.Length)
			{
			Segment child = new Segment(reader);

			switch(child.ID)
				{
                case(Group.C_MATMAPFILE): string name = parseString(reader,child);
                                          mat.Texture.Name = name;
                                          break;

				default: parseSkip(reader,child); break;
				}

			sector.BytesRead += child.BytesRead;
			}
		}

	private Data3DS parseObject(BinaryReader reader, Segment sector)
		{

		return(parseObject(reader,sector,new Data3DS()));
		}

	private Data3DS parseObject(BinaryReader reader, Segment sector, Data3DS cmp)
		{
		while(sector.BytesRead < sector.Length)
		    {
			Segment child = new Segment(reader);

			switch(child.ID)
			    {
                case(Group.C_OBJECT_MATERIAL): AeonMaterial mat = AeonMaterials.Default;
                                               String matName = parseString(reader,child);
					                           Console.WriteLine(" Uses Material: {0}",matName);
		                           
                                               if (!materials.TryGetValue(matName,out mat))
                                                  {
						                          Console.WriteLine(" Warning: Material '{0}' not found. ",matName);
						                          parseSkip(reader,child);
					                              }

                                               else{
						                           MaterialFaces mf = new MaterialFaces();
						                           mf.Material = mat;
					
						                           UInt16 nfaces = reader.ReadUInt16();
						                           child.BytesRead += 2;
						                          
						                           mf.Faces = new UInt16[nfaces];						
						                           for(int ii=0; ii<nfaces; ii++)
						                              {
							                          mf.Faces[ii] = reader.ReadUInt16();
							                          child.BytesRead += 2;
						                              }
					
						                           cmp.Materials.Add(mf);
					                               }
					                           break;

				case(Group.C_OBJECT_UV): UInt16 texels = reader.ReadUInt16();
					                     child.BytesRead += 2;
					                     Console.WriteLine(" TexCoords: {0}",texels);

					                     cmp.Texels = new Vertex[texels];
					                     for(int cnt=0;cnt<texels;cnt++)
                                             {
						                     cmp.Texels[cnt] = new Vertex(0,0,0,reader.ReadSingle(),reader.ReadSingle());
                                             }
					
					                     child.BytesRead += (texels*(4*2));
					                     break;

                case(Group.C_OBJECT_FACES): cmp.Indices = ReadIndices(reader,child);
    					                    if (child.BytesRead<child.Length) parseObject(reader,child,cmp);
					                        break;

                case(Group.C_OBJECT_VERTICES): cmp.Vertices = ReadVertices(reader,child); break;
			    case(Group.C_OBJECT_MESH): parseObject(reader,child,cmp); break;

				default: parseSkip(reader,child); break;
			    }

			sector.BytesRead += child.BytesRead;
		    }
        //*/
		return(cmp);
		}

	private Vertex[] ReadVertices(BinaryReader reader, Segment sector)
		{
		UInt16 numVerts = reader.ReadUInt16();
		sector.BytesRead += 2;
		Console.WriteLine(" Vertices: {0}",numVerts);

		Vertex[] verts = new Vertex[numVerts];
		for(int cnt=0;cnt<verts.Length;cnt++)
		    {
			Single f1 = reader.ReadSingle();
			Single f2 = reader.ReadSingle();
			Single f3 = reader.ReadSingle();
			Vertex v = new Vertex(f1,f3,-f2);
			
			// Track boundaries:
			if (v.X>maxX) maxX = v.X;
			if (v.Y>maxY) maxY = v.Y;
			if (v.Z>maxZ) maxZ = v.Z;			
			if (v.X<minX) minX = v.X;
			if (v.Y<minY) minY = v.Y;
			if (v.Z<minZ) minZ = v.Z;
			
			verts[cnt] = v;
		    }

		sector.BytesRead += verts.Length*(3*4);
		return(verts);
		}

	private Vertex[] ReadIndices(BinaryReader reader, Segment sector)
		{
		UInt16 numIdcs = reader.ReadUInt16();
		sector.BytesRead += 2;
		Console.WriteLine("	Indices: {0}",numIdcs);

		Vertex[] triangles = new Vertex[numIdcs];
		for(int cnt=0;cnt<triangles.Length;cnt++)
		    {
			triangles[cnt] = new Vertex(reader.ReadUInt16(),reader.ReadUInt16(),reader.ReadUInt16());
			reader.ReadUInt16();
		    }

		sector.BytesRead += (2*4)*triangles.Length;

		return(triangles);
		}

    }


}
